import { isServer } from "./is";

const ieVersion = isServer ? 0 : Number((document as any).documentMode);
const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
const MOZ_HACK_REGEXP = /^moz([A-Z])/;

export interface ViewportOffsetResult {
    left: number;
    top: number;
    right: number;
    bottom: number;
    rightIncludeBody: number;
    bottomIncludeBody: number;
}

/* istanbul ignore next */
const trim = function(string: string) {
    return (string || "").replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, "");
};

/* istanbul ignore next */
const camelCase = function(name: string) {
    return name
        .replace(SPECIAL_CHARS_REGEXP, function(_, __, letter, offset) {
            return offset ? letter.toUpperCase() : letter;
        })
        .replace(MOZ_HACK_REGEXP, "Moz$1");
};

/* istanbul ignore next */
export function hasClass(el: Element, cls: string) {
    if (!el || !cls) return false;
    if (cls.indexOf(" ") !== -1) {
        throw new Error("className should not contain space.");
    }
    if (el.classList) {
        return el.classList.contains(cls);
    } else {
        return (" " + el.className + " ").indexOf(" " + cls + " ") > -1;
    }
}

/* istanbul ignore next */
export function addClass(el: Element, cls: string) {
    if (!el) return;
    let curClass = el.className;
    const classes = (cls || "").split(" ");

    for (let i = 0, j = classes.length; i < j; i++) {
        const clsName = classes[i];
        if (!clsName) continue;

        if (el.classList) {
            el.classList.add(clsName);
        } else if (!hasClass(el, clsName)) {
            curClass += " " + clsName;
        }
    }
    if (!el.classList) {
        el.className = curClass;
    }
}

/* istanbul ignore next */
export function removeClass(el: Element, cls: string) {
    if (!el || !cls) return;
    const classes = cls.split(" ");
    let curClass = " " + el.className + " ";

    for (let i = 0, j = classes.length; i < j; i++) {
        const clsName = classes[i];
        if (!clsName) continue;

        if (el.classList) {
            el.classList.remove(clsName);
        } else if (hasClass(el, clsName)) {
            curClass = curClass.replace(" " + clsName + " ", " ");
        }
    }
    if (!el.classList) {
        el.className = trim(curClass);
    }
}

export function getBoundingClientRect(element: Element): DOMRect | number {
    if (!element || !element.getBoundingClientRect) {
        return 0;
    }
    return element.getBoundingClientRect();
}

/**
 * 获取当前元素的left、top偏移
 *   left：元素最左侧距离文档左侧的距离
 *   top:元素最顶端距离文档顶端的距离
 *   right:元素最右侧距离文档右侧的距离
 *   bottom：元素最底端距离文档底端的距离
 *   rightIncludeBody：元素最左侧距离文档右侧的距离
 *   bottomIncludeBody：元素最底端距离文档最底部的距离
 *
 * @description:
 */
export function getViewportOffset(element: Element): ViewportOffsetResult {
    const doc = document.documentElement;

    const docScrollLeft = doc.scrollLeft;
    const docScrollTop = doc.scrollTop;
    const docClientLeft = doc.clientLeft;
    const docClientTop = doc.clientTop;

    const pageXOffset = window.pageXOffset;
    const pageYOffset = window.pageYOffset;

    const box = getBoundingClientRect(element);

    const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect;

    const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0);
    const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0);
    const offsetLeft = retLeft + pageXOffset;
    const offsetTop = rectTop + pageYOffset;

    const left = offsetLeft - scrollLeft;
    const top = offsetTop - scrollTop;

    const clientWidth = window.document.documentElement.clientWidth;
    const clientHeight = window.document.documentElement.clientHeight;
    return {
        left: left,
        top: top,
        right: clientWidth - rectWidth - left,
        bottom: clientHeight - rectHeight - top,
        rightIncludeBody: clientWidth - left,
        bottomIncludeBody: clientHeight - top
    };
}

/* istanbul ignore next */
export const on = function(
    element: HTMLElement | Document | Window,
    event: string,
    handler: EventListenerOrEventListenerObject
): void {
    if (element && event && handler) {
        element.addEventListener(event, handler, false);
    }
};

/* istanbul ignore next */
export const off = function(
    element: HTMLElement | Document | Window,
    event: string,
    handler: any
): void {
    if (element && event && handler) {
        element.removeEventListener(event, handler, false);
    }
};

/* istanbul ignore next */
export const once = function(el: HTMLElement, event: string, fn: EventListener): void {
    const listener = function(this: any, ...args: unknown[]) {
        if (fn) {
            // @ts-ignore
            fn.apply(this, args);
        }
        off(el, event, listener);
    };
    on(el, event, listener);
};

/* istanbul ignore next */
export const getStyle =
    ieVersion < 9
        ? function(element: Element | any, styleName: string) {
            if (isServer) return;
            if (!element || !styleName) return null;
            styleName = camelCase(styleName);
            if (styleName === "float") {
                styleName = "styleFloat";
            }
            try {
                switch (styleName) {
                    case "opacity":
                        try {
                            return element.filters.item("alpha").opacity / 100;
                        } catch (e) {
                            return 1.0;
                        }
                    default:
                        return element.style[styleName] || element.currentStyle
                            ? element.currentStyle[styleName]
                            : null;
                }
            } catch (e) {
                return element.style[styleName];
            }
        }
        : function(element: Element | any, styleName: string) {
            if (isServer) return;
            if (!element || !styleName) return null;
            styleName = camelCase(styleName);
            if (styleName === "float") {
                styleName = "cssFloat";
            }
            try {
                const computed = (document as any).defaultView.getComputedStyle(element, "");
                return element.style[styleName] || computed ? computed[styleName] : null;
            } catch (e) {
                return element.style[styleName];
            }
        };

/* istanbul ignore next */
export function setStyle(element: Element | any, styleName: any, value: any) {
    if (!element || !styleName) return;

    if (typeof styleName === "object") {
        for (const prop in styleName) {
            if (Object.prototype.hasOwnProperty.call(styleName, prop)) {
                setStyle(element, prop, styleName[prop]);
            }
        }
    } else {
        styleName = camelCase(styleName);
        if (styleName === "opacity" && ieVersion < 9) {
            element.style.filter = isNaN(value) ? "" : "alpha(opacity=" + value * 100 + ")";
        } else {
            element.style[styleName] = value;
        }
    }
}

/* istanbul ignore next */
export const isScroll = (el: Element, vertical: any) => {
    if (isServer) return;

    const determinedDirection = vertical !== null || vertical !== undefined;
    const overflow = determinedDirection
        ? vertical
            ? getStyle(el, "overflow-y")
            : getStyle(el, "overflow-x")
        : getStyle(el, "overflow");

    return overflow.match(/(scroll|auto)/);
};

/* istanbul ignore next */
export const getScrollContainer = (el: Element, vertical?: any) => {
    if (isServer) return;

    let parent: any = el;
    while (parent) {
        if ([window, document, document.documentElement].includes(parent)) {
            return window;
        }
        if (isScroll(parent, vertical)) {
            return parent;
        }
        parent = parent.parentNode;
    }

    return parent;
};

/* istanbul ignore next */
export const isInContainer = (el: Element, container: any) => {
    if (isServer || !el || !container) return false;

    const elRect = el.getBoundingClientRect();
    let containerRect;

    if ([window, document, document.documentElement, null, undefined].includes(container)) {
        containerRect = {
            top: 0,
            right: window.innerWidth,
            bottom: window.innerHeight,
            left: 0
        };
    } else {
        containerRect = container.getBoundingClientRect();
    }

    return (
        elRect.top < containerRect.bottom &&
        elRect.bottom > containerRect.top &&
        elRect.right > containerRect.left &&
        elRect.left < containerRect.right
    );
};
